package com.gmail.bernabe.laurent.java_swing.rubik_cube_edi.ihm;

// Rubik's Cube 3D simulator
// Karl Hornell, March 11 1996
// Last modified October 6
// Adapted by Laurent BERNABE on July 2011

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.image.BufferedImage;

import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

import com.gmail.bernabe.laurent.java_swing.rubik_cube_edi.math.VectorUtils;

public final class Rubik extends JPanel {
	private static final long serialVersionUID = 1L;
	private int i,j,k,n,o,p,q,lastX,lastY,dx,dy;
	private int rectX[],rectY[];
	private Color colList[],bgcolor;
	private final double sideVec[]={0,0,1,0,0,-1,0,-1,0,1,0,0,0,1,0,-1,0,0}; //Norm vects
	private final double corners[]={-1,-1,-1,1,-1,-1,1,1,-1,-1,1,-1,
			-1,-1,1,1,-1,1,1,1,1,-1,1,1}; // Vertex co-ordinates
	private double topCorners[],botCorners[];
	private final int sides[]={4,5,6,7,3,2,1,0,0,1,5,4,1,2,6,5,2,3,7,6,0,4,7,3};
	private final int nextSide[]={2,3,4,5, 4,3,2,5, 1,3,0,5, 1,4,0,2, 1,5,0,3, 2,0,4,1};
	private final int mainBlocks[]={0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3};
	private final int twistDir[]={-1,1,-1,1, -1,1,-1,1, 1,1,1,1, 1,-1,1,-1, 1,1,1,1, -1,1,-1,1};
	private final int colDir[]={-1,-1,1,-1,1,-1};
	private final int circleOrder[]={0,1,2,5,8,7,6,3};
	private int topBlocks[],botBlocks[];
	private int sideCols[],sideW,sideH;
	private int dragReg,twistSide=-1;
	private int nearSide[],buffer[]; // Which side belongs to dragCorn
	private double dragCorn[],dragDir[];
	private double eye[]={0.3651,0.1826,-0.9129}; //Initial observer co-ordinate axes view
	private double eX[]={0.9309,-0.0716,0.3581}; // (sideways)
	private double eY[]; // (vertical)
	private double Teye[],TeX[],TeY[];
	private double light[],temp[]={0,0,0},temp2[]={0,0,0},newCoord[];
	private double sx,sy,sdxh,sdyh,sdxv,sdyv,d,t1,t2,t3,t4,t5,t6;
	private double phi,phibase=0,Cphi,Sphi,currDragDir[];

	private boolean naturalState=true,twisting=false,OKtoDrag=false;
	private Graphics offGraphics;
	private Image offImage;
	private VectorUtils vectorUtils = new VectorUtils();

	public Rubik(int sizeX, int sizeY, String str_bgColor) {
		setSize(sizeX, sizeY);
		
		addMouseListener(new MouseAdapter(){

			@Override
			public void mousePressed(MouseEvent e) {
				mouseDown(e);
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				mouseUp();
			}
			
		});
		
		addMouseMotionListener(new MouseMotionAdapter() {

			@Override
			public void mouseDragged(MouseEvent e) {
				mouseDrag(e);
			}
			
		});
		
		KeyStroke keyStroke_r = KeyStroke.getKeyStroke('r');
		KeyStroke keyStroke_s = KeyStroke.getKeyStroke('s');
		getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke_r, "RESET");
		getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke_s, "SCRAMBLE");
		getActionMap().put("RESET", new AbstractAction() {
			

			@Override
			public void actionPerformed(ActionEvent e) {
				reset();
			}
			
			private static final long serialVersionUID = 1L;
			
		});
		
		getActionMap().put("SCRAMBLE", new AbstractAction() {
			
			@Override
			public void actionPerformed(ActionEvent e) {
				scramble();
			}
			
			private static final long serialVersionUID = 1L;
			
		});
		
		rectX=new int[4];
		rectY=new int[4];
		newCoord=new double[16];	// Projected co-ordinates (on screen)
		dragDir=new double[24];
		dragCorn=new double[96];
		topCorners=new double[24];	// Vertex co-ordinate storage
		botCorners=new double[24];	// for sub-cubes during twist
		topBlocks=new int[24];
		botBlocks=new int[24];
		buffer=new int[12];
		nearSide=new int[12];
		light=new double[3];
		Teye=new double[3];
		TeX=new double[3];
		TeY=new double[3];
		currDragDir=new double[2];
		eY=new double[3];

		vectorUtils.vecProd(eye,0,eX,0,eY,0);	// Fix y axis of observer co-ordinate system
		vectorUtils.normalize(eY,0);
		colList=new Color[120];
		for (i=0;i<20;i++) {
			colList[i]=new Color(i*6,84+i*9,i*6);		// Green
			colList[i+20]=new Color(i*6,i*6,84+i*9);		// Blue
			colList[i+40]=new Color(84+i*9,i*5,i*5);		// Red
			colList[i+60]=new Color(103+i*8,103+i*8,103+i*8);	// White
			colList[i+80]=new Color(92+i*8,55+i*8,i*2);	// Orange
			colList[i+100]=new Color(74+i*8,74+i*8,i*4);	// Yellow
			
		}
		sideCols=new int[54];
		for (i=0;i<54;i++) sideCols[i]=i/9;
		bgcolor=findBGColor(str_bgColor);
		repaint();
	}

	private Color findBGColor(String s) {	// Convert hexadecimal RGB parameter to color
		int hex[];
		String h="0123456789abcdef";
		Color c;
		hex=new int[6];
		if ((s!=null)&&(s.length()==6)) {
			for (i=0;i<6;i++)
				for (j=0;j<16;j++)
					if (Character.toLowerCase(s.charAt(i))==h.charAt(j)) hex[i]=j;
			c=new Color(hex[0]*16+hex[1],hex[2]*16+hex[3],hex[4]*16+hex[5]);
		} else
			c=Color.lightGray; // Default
		return c;
	}

	public void cutUpCube() {	// Produce large and small sub-cube for twisting
		boolean check;

		for (i=0;i<24;i++) {	// Copy main coordinate data
			topCorners[i]=corners[i];
			botCorners[i]=corners[i];
		}
		vectorUtils.copyVec(sideVec,3*twistSide,temp,0); //Start manipulating & build new parts
		vectorUtils.copyVec(temp,0,temp2,0); // Fix new co-ordinates. Some need to be altered.
		vectorUtils.scalMult(temp,0,1.3333);
		vectorUtils.scalMult(temp2,0,0.6667);
		for (i=0;i<8;i++) {
			check=false;
			for (j=0;j<4;j++)
				if (i==sides[twistSide*4+j]) check=true;
			if (check)
				vectorUtils.subVec(temp2,0,botCorners,i*3);
			else
				vectorUtils.addVec(temp,0,topCorners,i*3);
		}

		// The sub-cubes need information about which colored fields belong to them.
		for (i=0;i<24;i++) { //Fix the sub-cube blockings. First copy data from main
			topBlocks[i]=mainBlocks[i]; // Large sub-cube data
			botBlocks[i]=mainBlocks[i]; // Small sub-cube data
		}
		for (i=0;i<6;i++) {
			if (i==twistSide) {
				botBlocks[i*4+1]=0; // Large sub-cube is blank on top
				botBlocks[i*4+3]=0;
			} else {
				k=-1;
				for (j=0;j<4;j++)
					if (nextSide[i*4+j]==twistSide) k=j;
				switch (k) { // Twisted side adjacent to...
				case 0: // Up side?
					topBlocks[i*4+3]=1;
					botBlocks[i*4+2]=1;
					break;
				case 1: // Right side?
					topBlocks[i*4]=2;
					botBlocks[i*4+1]=2;
					break;
				case 2: // Down side?
					topBlocks[i*4+2]=2;
					botBlocks[i*4+3]=2;
					break;
				case 3: // Left side?
					topBlocks[i*4+1]=1;
					botBlocks[i*4]=1;
					break;
				case -1: // None
					topBlocks[i*4+1]=0; // Small sub-cube is blank on bottom
					topBlocks[i*4+3]=0;
					break;
				}
			}
		}
	}

	public void scramble() {
		twisting=false;
		naturalState=true;
		for (i=0;i<20;i++)
			colorTwist((int)(Math.random()*6),(int)(Math.random()*3+1));
		repaint();
	}
	
	public void reset() {
		twisting=false;
		naturalState=true;
		for (i=0;i<54;i++) sideCols[i]=i/9;
		repaint();
	}

	public boolean mouseDrag(MouseEvent e) {
		boolean check;
		double x1,x2,y1,y2,alpha,beta;
		int x = e.getX();
		int y = e.getY();

		if ((!twisting)&&(OKtoDrag)) {
			OKtoDrag=false;
			check=false;
			for (i=0;i<dragReg;i++) { // Check if inside a drag region
				x1=dragCorn[i*8+1]-dragCorn[i*8];
				x2=dragCorn[i*8+5]-dragCorn[i*8+4];
				y1=dragCorn[i*8+3]-dragCorn[i*8];
				y2=dragCorn[i*8+7]-dragCorn[i*8+4];
				alpha=(y2*(lastX-dragCorn[i*8])-y1*(lastY-dragCorn[i*8+4]))/
				(x1*y2-y1*x2);
				beta=(-x2*(lastX-dragCorn[i*8])+x1*(lastY-dragCorn[i*8+4]))/
				(x1*y2-y1*x2);
				if ((alpha>0)&&(alpha<1)&&(beta>0)&&(beta<1)) { // We're in
					currDragDir[0]=dragDir[i*2];
					currDragDir[1]=dragDir[i*2+1];
					d=currDragDir[0]*(x-lastX)+currDragDir[1]*(y-lastY);
					d=d*d/((currDragDir[0]*currDragDir[0]+currDragDir[1]*currDragDir[1])*
							((x-lastX)*(x-lastX)+(y-lastY)*(y-lastY)));
					if (d>0.6) {
						check=true;
						twistSide=nearSide[i];
						i=100;
					}
				}
			}
			if (check) { // We're twisting
				if (naturalState) { // The cube still hasn't been split up
					cutUpCube();
					naturalState=false;
				}
				twisting=true;
				phi=0.02*(currDragDir[0]*(x-lastX)+currDragDir[1]*(y-lastY))/
				Math.sqrt(currDragDir[0]*currDragDir[0]+currDragDir[1]*currDragDir[1]);
				repaint();
				return false;
			}
		}

		OKtoDrag=false;
		if (!twisting) { // Normal rotation
			dx=lastX-x; // Vertical shift
			vectorUtils.copyVec(eX,0,temp,0);
			vectorUtils.scalMult(temp,0,((double)dx)*0.016);
			vectorUtils.addVec(temp,0,eye,0);
			vectorUtils.vecProd(eY,0,eye,0,eX,0);
			vectorUtils.normalize(eX,0);
			vectorUtils.normalize(eye,0);
			dy=y-lastY; // Horizontal shift
			vectorUtils.copyVec(eY,0,temp,0);
			vectorUtils.scalMult(temp,0,((double)dy)*0.016);
			vectorUtils.addVec(temp,0,eye,0);
			vectorUtils.vecProd(eye,0,eX,0,eY,0);
			vectorUtils.normalize(eY,0);
			vectorUtils.normalize(eye,0);
			lastX=x;
			lastY=y;
			repaint();
		} else { // Twist, compute twisting angle phi
			phi=0.02*(currDragDir[0]*(x-lastX)+currDragDir[1]*(y-lastY))/
			Math.sqrt(currDragDir[0]*currDragDir[0]+currDragDir[1]*currDragDir[1]);
			repaint();
		}
		return false;
	}

	public boolean mouseDown(MouseEvent e) {
		lastX=e.getX();
		lastY=e.getY();
		OKtoDrag=true;
		return false;
	}

	public void mouseUp() {
		int quads;
		double qu;
		if (twisting) { // We have let go of the mouse when twisting
			twisting=false;
			phibase+=phi; // Save twist angle
			phi=0;
			qu=phibase;
			while (qu<0) qu+=125.662;
			quads=((int)(qu*3.183));
			if (((quads % 5)==0)||((quads % 5)==4)) { // Close enough to a corner?
				quads=((quads+1)/5) % 4;
				if (colDir[twistSide]<0) quads=(4-quads) % 4;
				phibase=0;
				naturalState=true; // Return the cube to its natural state
				colorTwist(twistSide,quads); // and shift the colored fields
			}
			repaint();
		}
	}

	public void colorTwist(int sideNum, int quads) { // Shift colored fields
		int i,j,k,l=0;
		k=quads*2; // quads = number of 90-degree multiples
		for (i=0;i<8;i++) {
			buffer[k]=sideCols[sideNum*9+circleOrder[i]];
			k=(k+1) % 8;
		}
		for (i=0;i<8;i++) sideCols[sideNum*9+circleOrder[i]]=buffer[i];
		k=quads*3;
		for (i=0;i<4;i++) {
			for (j=0;j<4;j++)
				if (nextSide[nextSide[sideNum*4+i]*4+j]==sideNum) l=j;
			for (j=0;j<3;j++) {
				switch(l) {
				case 0:
					buffer[k]=sideCols[nextSide[sideNum*4+i]*9+j];
					break;
				case 1:
					buffer[k]=sideCols[nextSide[sideNum*4+i]*9+2+3*j];
					break;
				case 2:
					buffer[k]=sideCols[nextSide[sideNum*4+i]*9+8-j];
					break;
				case 3:
					buffer[k]=sideCols[nextSide[sideNum*4+i]*9+6-3*j];
					break;
				}
				k=(k+1) % 12;
			}
		}
		k=0;
		for (i=0;i<4;i++) {
			for (j=0;j<4;j++)
				if (nextSide[nextSide[sideNum*4+i]*4+j]==sideNum) l=j;
			for (j=0;j<3;j++) {
				switch(l) {
				case 0:
					sideCols[nextSide[sideNum*4+i]*9+j]=buffer[k];
					break;
				case 1:
					sideCols[nextSide[sideNum*4+i]*9+2+3*j]=buffer[k];
					break;
				case 2:
					sideCols[nextSide[sideNum*4+i]*9+8-j]=buffer[k];
					break;
				case 3:
					sideCols[nextSide[sideNum*4+i]*9+6-3*j]=buffer[k];
					break;
				}
				k++;
			}
		}
	}

	public void paint(Graphics g) {
		int sizeX = getSize().width;
		int sizeY = getSize().height;
		offImage=new BufferedImage(sizeX, sizeY, BufferedImage.TYPE_3BYTE_BGR); // Double buffer
		offGraphics=offImage.getGraphics();
		dragReg=0;
		offGraphics.setColor(bgcolor); // Clear drawing buffer
		offGraphics.fillRect(0,0,sizeX, sizeY);
		if (naturalState)
			fixBlock(eye,eX,eY,corners,mainBlocks,0, sizeX, sizeY); // Draw cube
		else {
			vectorUtils.copyVec(eye,0,Teye,0); // In twisted state? Compute top observer
			vectorUtils.copyVec(eX,0,TeX,0);
			Cphi=Math.cos(phi+phibase);
			Sphi=-Math.sin(phi+phibase);
			switch(twistSide) { // Twist around which axis?
			case 0: // z
				Teye[0]=Cphi*eye[0]+Sphi*eye[1];
				TeX[0]=Cphi*eX[0]+Sphi*eX[1];
				Teye[1]=-Sphi*eye[0]+Cphi*eye[1];
				TeX[1]=-Sphi*eX[0]+Cphi*eX[1];
				break;
			case 1: // -z
				Teye[0]=Cphi*eye[0]-Sphi*eye[1];
				TeX[0]=Cphi*eX[0]-Sphi*eX[1];
				Teye[1]=Sphi*eye[0]+Cphi*eye[1];
				TeX[1]=Sphi*eX[0]+Cphi*eX[1];
				break;
			case 2: // -y
				Teye[0]=Cphi*eye[0]-Sphi*eye[2];
				TeX[0]=Cphi*eX[0]-Sphi*eX[2];
				Teye[2]=Sphi*eye[0]+Cphi*eye[2];
				TeX[2]=Sphi*eX[0]+Cphi*eX[2];
				break;
			case 3: // x
				Teye[1]=Cphi*eye[1]+Sphi*eye[2];
				TeX[1]=Cphi*eX[1]+Sphi*eX[2];
				Teye[2]=-Sphi*eye[1]+Cphi*eye[2];
				TeX[2]=-Sphi*eX[1]+Cphi*eX[2];
				break;
			case 4: // y
				Teye[0]=Cphi*eye[0]+Sphi*eye[2];
				TeX[0]=Cphi*eX[0]+Sphi*eX[2];
				Teye[2]=-Sphi*eye[0]+Cphi*eye[2];
				TeX[2]=-Sphi*eX[0]+Cphi*eX[2];
				break;
			case 5: // -x
				Teye[1]=Cphi*eye[1]-Sphi*eye[2];
				TeX[1]=Cphi*eX[1]-Sphi*eX[2];
				Teye[2]=Sphi*eye[1]+Cphi*eye[2];
				TeX[2]=Sphi*eX[1]+Cphi*eX[2];
				break;
			}
			vectorUtils.vecProd(Teye,0,TeX,0,TeY,0);
			if (vectorUtils.scalProd(eye,0,sideVec,twistSide*3)<0) { //Top facing away? Draw it
				fixBlock(Teye,TeX,TeY,topCorners,topBlocks,2, sizeX, sizeY);
				fixBlock(eye,eX,eY,botCorners,botBlocks,1, sizeX, sizeY);
			} else {
				fixBlock(eye,eX,eY,botCorners,botBlocks,1, sizeX, sizeY);
				fixBlock(Teye,TeX,TeY,topCorners,topBlocks,2, sizeX, sizeY);
			}
		}
		g.drawImage(offImage,0,0,this);
	}

	public void update(Graphics g) {
		paint(g);
	}

	// Draw cube or sub-cube
	public void fixBlock(double beye[],double beX[],double beY[],
			double bcorners[],int bblocks[],int mode, int sizeX, int sizeY) {
		
		int cubeSize = sizeX < sizeY ? sizeX : sizeY;
		
		vectorUtils.copyVec(beye,0,light,0);
		vectorUtils.scalMult(light,0,-3);
		vectorUtils.addVec(beX,0,light,0);
		vectorUtils.subVec(beY,0,light,0);

		for (i=0;i<8;i++) { // Project 3D co-ordinates into 2D screen ones
			newCoord[i*2]=(sizeX/2+0.26325*cubeSize*vectorUtils.scalProd(bcorners,i*3,beX,0));
			newCoord[i*2+1]=(sizeY/2-0.26325*cubeSize*vectorUtils.scalProd(bcorners,i*3,beY,0));
		}

		for (i=0;i<6;i++) {
			if (vectorUtils.scalProd(beye,0,sideVec,3*i)>0.001) { // Face towards us? Draw it.
				k=(int)(9.6*(1-vectorUtils.cosAng(light,0,sideVec,3*i)));
				offGraphics.setColor(Color.black);
				for (j=0;j<4;j++) { // Find corner co-ordinates
					rectX[j]=(int)newCoord[2*sides[i*4+j]];
					rectY[j]=(int)newCoord[2*sides[i*4+j]+1];
				}
				offGraphics.fillPolygon(rectX,rectY,4); // First draw black
				sideW=bblocks[i*4+1]-bblocks[i*4];
				sideH=bblocks[i*4+3]-bblocks[i*4+2];
				if (sideW>0) {
					sx=newCoord[2*sides[i*4]];
					sy=newCoord[2*sides[i*4]+1];
					sdxh=(newCoord[2*sides[i*4+1]]-sx)/sideW;
					sdxv=(newCoord[2*sides[i*4+3]]-sx)/sideH;
					sdyh=(newCoord[2*sides[i*4+1]+1]-sy)/sideW;
					sdyv=(newCoord[2*sides[i*4+3]+1]-sy)/sideH;
					p=bblocks[i*4+2];
					for (n=0;n<sideH;n++) { // Then draw colored fields
						q=bblocks[i*4];
						for (o=0;o<sideW;o++) {
							rectX[0]=(int)(sx+(o+0.1)*sdxh+(n+0.1)*sdxv);
							rectX[1]=(int)(sx+(o+0.9)*sdxh+(n+0.1)*sdxv);
							rectX[2]=(int)(sx+(o+0.9)*sdxh+(n+0.9)*sdxv);
							rectX[3]=(int)(sx+(o+0.1)*sdxh+(n+0.9)*sdxv);
							rectY[0]=(int)(sy+(o+0.1)*sdyh+(n+0.1)*sdyv);
							rectY[1]=(int)(sy+(o+0.9)*sdyh+(n+0.1)*sdyv);
							rectY[2]=(int)(sy+(o+0.9)*sdyh+(n+0.9)*sdyv);
							rectY[3]=(int)(sy+(o+0.1)*sdyh+(n+0.9)*sdyv);
							offGraphics.setColor(colList[20*sideCols[i*9+p*3+q]+k]);
							offGraphics.fillPolygon(rectX,rectY,4);
							q++;
						}
						p++;
					}
				}
				switch (mode) { // Determine allowed drag regions and directions
				case 0: // Just the normal cube
					t1=sx;
					t2=sy;
					t3=sdxh;
					t4=sdyh;
					t5=sdxv;
					t6=sdyv;
					for (j=0;j<4;j++) {
						dragCorn[8*dragReg]=t1;
						dragCorn[8*dragReg+4]=t2;
						dragCorn[8*dragReg+3]=t1+t5;
						dragCorn[8*dragReg+7]=t2+t6;
						t1=t1+t3*3;
						t2=t2+t4*3;
						dragCorn[8*dragReg+1]=t1;
						dragCorn[8*dragReg+5]=t2;
						dragCorn[8*dragReg+2]=t1+t5;
						dragCorn[8*dragReg+6]=t2+t6;
						dragDir[dragReg*2]=t3*twistDir[i*4+j];
						dragDir[dragReg*2+1]=t4*twistDir[i*4+j];
						d=t3;
						t3=t5;
						t5=-d;
						d=t4;
						t4=t6;
						t6=-d;
						nearSide[dragReg]=nextSide[i*4+j];
						dragReg++;
					}
					break;
				case 1: // The large sub-cube
					break;
				case 2: // The small sub-cube (twistable part)
					if ((i!=twistSide)&&(sideW>0)) {
						if (sideW==3) // Determine positive drag direction
							if (bblocks[i*4+2]==0) {
								dragDir[dragReg*2]=sdxh*twistDir[i*4];
								dragDir[dragReg*2+1]=sdyh*twistDir[i*4];
							} else {
								dragDir[dragReg*2]=-sdxh*twistDir[i*4+2];
								dragDir[dragReg*2+1]=-sdyh*twistDir[i*4+2];
							} else if (bblocks[i*4]==0) {
								dragDir[dragReg*2]=-sdxv*twistDir[i*4+3];
								dragDir[dragReg*2+1]=-sdyv*twistDir[i*4+3];
							} else {
								dragDir[dragReg*2]=sdxv*twistDir[i*4+1];
								dragDir[dragReg*2+1]=sdyv*twistDir[i*4+1];
							}
						for (j=0;j<4;j++) {
							dragCorn[dragReg*8+j]=newCoord[2*sides[i*4+j]];
							dragCorn[dragReg*8+4+j]=newCoord[2*sides[i*4+j]+1];
						}
						nearSide[dragReg]=twistSide;
						dragReg++;
					}
					break;
				}
			}
		}
	}
}
